package com.unimod.fsml.model.transform;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.evelopers.unimod.core.stateworks.Action;
import com.evelopers.unimod.core.stateworks.ControlledObjectHandler;
import com.evelopers.unimod.core.stateworks.Event;
import com.evelopers.unimod.core.stateworks.EventProviderHandler;
import com.evelopers.unimod.core.stateworks.Guard;
import com.evelopers.unimod.core.stateworks.Model;
import com.evelopers.unimod.core.stateworks.State;
import com.evelopers.unimod.core.stateworks.StateMachine;
import com.evelopers.unimod.core.stateworks.StateType;
import com.evelopers.unimod.core.stateworks.Transition;
import com.evelopers.unimod.plugin.eclipse.model.GNormalState;
import com.evelopers.unimod.plugin.eclipse.model.GStateMachineHandle;
import com.evelopers.unimod.plugin.eclipse.model.GSubmachineList;
import com.evelopers.unimod.plugin.eclipse.model.GTransition;
import com.evelopers.unimod.runtime.ControlledObject;
import com.evelopers.unimod.runtime.context.StateMachineContext;
import com.unimod.fsml.FSMLTokens;
import com.unimod.fsml.FSMLTokens.EVENT_TYPE;
import com.unimod.fsml.util.COHelper;

/**
 * Transforms fsml files into {@link StateMachine state machine} model.
 * 
 * @author Ivan Lagunov
 */
public class FSMLToModel implements ControlledObject {
    private static final Log log = LogFactory.getLog(FSMLToModel.class);
    
	private Model model;
	private StateMachine sm;
	private StateType sourceStateType;
	private State sourceState;
	private String targetStateName;
	private String guardString;
	private Guard guard;
	private String eventType;
	private List<Event> eventList;
	private String actionName;
	private List<Action> actionList;
	/**
	 * Map from state name to the list of unfinished transitions from this state.
	 */
	private Map<String, List<UnfinishedTransition>> unfinishedTransitions;
	/**
	 * Map from event provider class name to its instance name.
	 */
	private Map<String, String> eventProviders = new HashMap<String, String>();
    /**
     * Map from controlled object instance name to its full class name.
     */
    private Map<String, String> controlledObjects = new HashMap<String, String>();
    private StringBuilder className = new StringBuilder("");
    private Set<String> submachines = new HashSet<String>();
    private Set<String> states = new HashSet<String>();
    
    public FSMLToModel(Model model) {
    	this.model = model;
    	
        this.guardString = "";
        this.actionName = "";
        this.sourceStateType = StateType.NORMAL;
        this.unfinishedTransitions = new HashMap<String, List<UnfinishedTransition>>();
    }
    
	/**
	 * @unimod.action.descr create state machine
	 */
    @SuppressWarnings("unused")
	public void z1(StateMachineContext context) {
		String stateMachineName = COHelper.getTokenValue(context);
		sm = getStateMachine(stateMachineName);
		sourceState = sm.getTop();
		
		for (Entry<String, String> entry : eventProviders.entrySet()) {
			EventProviderHandler epHandler = model.createEventProviderHandler(entry.getValue(), entry.getKey());
			sm.createIncomingAssociation(epHandler);
		}
	}

	/**
	 * @unimod.action.descr save submachine
	 */
	public void z2(StateMachineContext context) {
		String innerStateMachineName = COHelper.getTokenValue(context);
		StateMachine innerStateMachine = getStateMachine(innerStateMachineName);

		sm.addStateMachine(innerStateMachine);
		submachines.add(innerStateMachineName);
	}

	/**
	 * @unimod.action.descr save source state type
	 */
	public void z3(StateMachineContext context) {
		String stype = COHelper.getTokenValue(context);
		if (stype.equals(FSMLTokens.STATE_TYPE.INITIAL)) {
			sourceStateType = StateType.INITIAL;
		} else if (stype.equals(FSMLTokens.STATE_TYPE.FINAL)) {
			sourceStateType = StateType.FINAL;
		}
	}

	/**
	 * @unimod.action.descr create source state
	 */
	public void z4(StateMachineContext context) {
		String stateName = COHelper.getTokenValue(context);
		State newState = sm.createState(stateName, sourceStateType);
		sourceState.addSubstate(newState);
		sourceState = newState;
		states.add(stateName);
		
		sourceStateType = StateType.NORMAL;
		
		// If there are some transitions to finish.
		if (unfinishedTransitions.containsKey(stateName)) {
			for (UnfinishedTransition unfinishedTransition : unfinishedTransitions.remove(stateName)) {
				createTransitions(unfinishedTransition.getSourceState(), sourceState, 
						unfinishedTransition.getGuard(), 
						unfinishedTransition.getEvents(), 
						unfinishedTransition.getActions());
			}
		}
	}

	/**
	 * @unimod.action.descr save guard string
	 */
	public void z5(StateMachineContext context) {
		String guardPart = COHelper.getTokenValue(context);
		
		if (!guardPart.equals(FSMLTokens.IF)) {
			guardString += guardPart;
		}
	}
	
	/**
	 * @unimod.action.descr create transitions and actions
	 */
    @SuppressWarnings("unused")
	public void z6(StateMachineContext context) {
    	if ((actionList != null) || (targetStateName != null)) {
	    	if ((null == eventType) && (null == eventList)) {
				eventType = EVENT_TYPE.ANY;
	    	}
	
	    	if (EVENT_TYPE.ANY.equals(eventType)) {
	    		eventType = null;
				eventList = new ArrayList<Event>();
				eventList.add(Event.ANY);
	    	}
	    	
			if (eventType != null) {
				saveActionsForState(sourceState, actionList, eventType);
				eventType = null;
			} else {
				saveTransitions(sourceState, targetStateName, guard, eventList, actionList);
				targetStateName = null;
		        guard = null;
			}

			actionList = null;
    	}
		eventList = null;
	}

	/**
	 * @unimod.action.descr add event
	 */
	public void z7(StateMachineContext context) {
		if (null == eventList) {
			eventList = new ArrayList<Event>();
		}

		String eventName = COHelper.getTokenValue(context);
		eventList.add(new Event(eventName));
	}

	/**
	 * @unimod.action.descr create guard
	 */
    @SuppressWarnings("unused")
    public void z8(StateMachineContext context) {
		if (guardString.length() != 0) { 
			guard = sm.createGuard(guardString);
			guardString = "";
		}
	}

	/**
	 * @unimod.action.descr save event type
	 */
	public void z9(StateMachineContext context) {
		eventType = COHelper.getTokenValue(context);
	}

	/**
	 * @unimod.action.descr save action name
	 */
	public void z10(StateMachineContext context) {
		actionName += COHelper.getTokenValue(context);
	}

	/**
	 * @unimod.action.descr add action
	 */
    @SuppressWarnings("unused")
	public void z11(StateMachineContext context) {
		if (null == actionList) {
			actionList = new ArrayList<Action>();
		}

		actionList.add(sm.createAction(actionName));
		actionName = "";
	}

	/**
	 * @unimod.action.descr save target state name
	 */
	public void z12(StateMachineContext context) {
		targetStateName = COHelper.getTokenValue(context);
	}


	/**
	 * @unimod.action.descr save event provider class
	 */
	public void z13(StateMachineContext context) {
		className.append(COHelper.getTokenValue(context));
	}

	/**
	 * @unimod.action.descr add event provider
	 */
	public void z14(StateMachineContext context) {
		String epClassName = className.toString();
		if (!eventProviders.containsKey(epClassName)) {
			String epName = generateEventProviderName();
			eventProviders.put(epClassName, epName);
			// Event providers will be created on state machine creation (z1).
		}
		className = new StringBuilder("");
	}

	/**
	 * @unimod.action.descr save controlled object class
	 */
	public void z15(StateMachineContext context) {
		className.append(COHelper.getTokenValue(context));
	}

	/**
	 * @unimod.action.descr add controlled object
	 */
	public void z16(StateMachineContext context) {
		String coName = COHelper.getTokenValue(context);
		String coClassName = className.toString();
		controlledObjects.put(coName, coClassName);
		ControlledObjectHandler coHandler = model.createControlledObjectHandler(coName, coClassName);
		sm.createOutgoingAssociation(coHandler);
		className = new StringBuilder("");
	}

	/**
	 * @unimod.action.descr add submachines
	 */
	public void z17(StateMachineContext context) {
		if (sourceState instanceof GNormalState) {
			GNormalState gns = (GNormalState) sourceState;
			for (Object o : gns.getSubMachineHandles().asCollection()) {
				GStateMachineHandle gsmh = (GStateMachineHandle) o;
				submachines.add(gsmh.getName());
			}
			gns.setSubMachineHandles(new GSubmachineList(submachines.toArray(new String[0])));
		} else {
			log.error("z2: Adding submachine to not normal state.");
		}
		submachines.clear();
	}

	/**
	 * @unimod.action.descr move to super state
	 */
	public void z18(StateMachineContext context) {
		sourceState = sourceState.getSuperstate();
	}

	/**
	 * Finds existing or creates new state machine in model.
	 * 
	 * @param stateMachineName name of state machine.
	 * @return state machine with the given name.
	 */
	private StateMachine getStateMachine(String stateMachineName) {
		StateMachine stateMachine = model.getStateMachine(stateMachineName);
		if (null == stateMachine) {
			stateMachine = model.createStateMachine(stateMachineName);
		}
		return stateMachine;
	}
	
	/**
	 * Creates transitions if target state was already added to state machine,
	 * saves transitions as unfinished otherwise.
	 * 
	 * @param sourceState the source state of each transition.
	 * @param targetStateName the name of target state of each transition.
	 * @param guard the guard condition on each transition.
	 * @param eventList the list of events for transitions.
	 * @param actionList the list of actions for each transition.
	 */
	private void saveTransitions(State sourceState, String targetStateName, Guard guard, List<Event> eventList, List<Action> actionList) {
		State targetState;
		if (null == targetStateName) {
			targetState = sourceState;
		} else {
			targetState = sm.findState(targetStateName); 
		}
		
		if (targetState != null) {
			createTransitions(sourceState, targetState, guard, eventList, actionList);
		} else {
			// Saves transition until targetState is initialized.
			UnfinishedTransition unfinishedTransition = new UnfinishedTransition(sourceState, eventList, actionList, guard);

			if (unfinishedTransitions.get(targetStateName) == null) {
				unfinishedTransitions.put(targetStateName, new ArrayList<UnfinishedTransition>());
			}
				
			unfinishedTransitions.get(targetStateName).add(unfinishedTransition);
		}
	}
	
	/**
	 * Creates transitions using specified data.
	 * 
	 * @param sourceState the source state of each transition.
	 * @param targetState the target state of each transition.
	 * @param guard the guard condition on each transition.
	 * @param eventList the list of events for transitions.
	 * @param actionList the list of actions for each transition.
	 */
	private void createTransitions(State sourceState, State targetState, Guard guard, List<Event> eventList, List<Action> actionList) {
		for (Event event : eventList) {
			GTransition transition = (GTransition) sm.createTransition(sourceState, targetState, guard, event);
			
			saveActionsForTransition(transition, actionList);
			transition.getLabel().update();
		}
	}
	
	/**
	 * Registers actions for specified transition. 
	 * 
	 * @param transition last parsed transition. 
	 * @param actionList list of actions to register.
	 */
	private void saveActionsForTransition(Transition transition, List<Action> actionList) {
		if (actionList != null) {
			for (Action action : actionList) {
				transition.addOutputAction(action);
			}		
		}
	}
	
	/**
	 * Registers actions for specified state.
	 * 
	 * @param state last parsed state.
	 * @param actionList list of actions to register.
	 * @param eventType the type of both event and actions. 
	 */
	private void saveActionsForState(State state, List<Action> actionList, String eventType) {
		if (eventType.equals(EVENT_TYPE.ENTER)) {
			for (Action action : actionList) {
				state.addOnEnterAction(action);
			}
		} else if (eventType.equals(EVENT_TYPE.EXIT)) {
			//TODO: this event type will be only in Unimod 2.
		}
	}
	
	/**
	 * @unimod.action.descr source is initial state
	 */
    @SuppressWarnings("unused")
	public boolean x1(StateMachineContext context) {
		if (null == sourceState) {
			log.warn("FSMLToModel: source state is null");
			return false;
		}
		
		return sourceState.getType().equals(StateType.INITIAL);
	}

	/**
	 * @unimod.action.descr source is final state
	 */
    @SuppressWarnings("unused")
	public boolean x2(StateMachineContext context) {
		if (null == sourceState) {
			log.warn("FSMLToModel: source state is null");
			return false;
		}
		
		return sourceState.getType().equals(StateType.FINAL);
	}

	/**
	 * @unimod.action.descr source is normal state
	 */
    @SuppressWarnings("unused")
	public boolean x3(StateMachineContext context) {
		if (null == sourceState) {
			log.warn("FSMLToModel: source state is null");
			return false;
		}
		
		return sourceState.getType().equals(StateType.NORMAL);
	}
    
	/**
	 * @unimod.action.descr event type is on enter 
	 */
    @SuppressWarnings("unused")
	public boolean x4(StateMachineContext context) {
    	return EVENT_TYPE.ENTER.equals(eventType);
    }    

    /**
	 * Returns an iterator over saved event providers.
	 * 
	 * @return an iterator over saved event providers.
	 */
	public Iterator<String> getEventProvidersIterator() {
		return eventProviders.keySet().iterator();
	}
	
	/**
	 * Returns an iterator over saved controlled objects' names.
	 * 
	 * @return an iterator over saved controlled objects' names.
	 */
	public Iterator<String> getControlledObjectsIterator() {
		return controlledObjects.keySet().iterator();
	}

	/**
	 * Returns the controlled object class name by given instance name.
	 * 
	 * @return the class name of controlled object.
	 */
	public String getControlledObjectClass(String coName) {
		return controlledObjects.get(coName);		
	}
	
	/**
	 * Returns an iterator over created states.
	 * 
	 * @return an iterator over created states.
	 */
	public Iterator<String> getStates() {
		return states.iterator();
	}
	
	private String generateEventProviderName() {
		return String.format("p%d", eventProviders.size() + 1);
	}
}
